package com.atguigu.gmall.controller;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 测试思路和步骤：
 *      1、先测试功能：微服务是一个实例，没有并发（计算正确）
 *      2、测试并发操作：微服务是一个实例，有并发，没有加本地锁（计算错误）
 *      3、测试并发操作：微服务是一个实例，有并发，加了加本地锁（计算正确）
 *      4、测试并发操作：微服务是多个实例，有并发，加了加本地锁（计算错误）
 *      5、测试并发操作：微服务是多个实例，有并发，加分布式锁（计算正确）
 */
@RestController
@RequestMapping("/redis/test")
public class DemoController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/data/write")
    public String dataWriteWithLocalLock() {

        // 1、设定 key
        String key = "lock:test:data:write";

        synchronized (this) {
            // 2、针对 key 对应的值做 +1 操作
            // 这里不能使用原子性操作，因为原子性操作无法暴露出并发问题
            // redisTemplate.opsForValue().increment(key);
            String number = redisTemplate.opsForValue().get(key);

            if (number == null) {
                number = "0";
            }

            Integer intNumber = Integer.parseInt(number) + 1;
            redisTemplate.opsForValue().set(key, intNumber.toString());
        }

        return "OK";
    }

    @GetMapping("/data/write/lock")
    public String dataWriteWithLock() throws InterruptedException {

        // 1、设定后面操作需要使用的 key
        // [1]设定业务数据使用的 key
        String dataKey = "lock:test:data:write";

        // [2]设定锁数据使用的 key
        String lockKey = "lock:test:lock:number";

        // ※为了实现防误删，根据 UUID 生成一个唯一值，作为锁释放时的依据
        String value = UUID.randomUUID().toString().replace("-", "").toUpperCase();

        // 2、尝试执行加锁
        // 锁数据没有过期时间
        // Boolean setResult = redisTemplate.opsForValue().setIfAbsent(lockKey, "some-value", 5, TimeUnit.MINUTES);

        // 给锁数据附加过期时间，目的是保证锁数据一定能够被释放
        // 使用 UUID 值作为锁数据中的 value 部分
        Boolean setResult = redisTemplate.opsForValue().setIfAbsent(lockKey, value, 5, TimeUnit.MINUTES);

        // 额外调用 expire() 方法设置过期时间，会导致操作从一步变成两步，从而不满足原子性
        // redisTemplate.expire(lockKey, 5, TimeUnit.MINUTES);

        if (setResult) {
            try {
                // 3、如果加锁成功，则执行业务操作
                String number = redisTemplate.opsForValue().get(dataKey);

                if (number == null) {
                    number = "0";
                }

                Integer intNumber = Integer.parseInt(number) + 1;

                redisTemplate.opsForValue().set(dataKey, intNumber.toString());
            } catch (NumberFormatException e) {
                throw new RuntimeException(e);
            } finally {
                // 4、业务操作完成后释放锁
                // ※把解锁操作放在 finally 块中，以确保执行
                // ※释放锁时，检查 value，看看是不是自己设置的 UUID
                // ※释放锁的操作如果分成两步，那就又不满足原子性了
                // String redisValue = redisTemplate.opsForValue().get(lockKey);
                // if (value.equals(redisValue)) {
                //     redisTemplate.delete(lockKey);
                // }

                // ※为了让释放锁的操作满足原子性，所以使用 LUA 脚本
                // [1]使用字符串的形式保存 LUA 脚本内容
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

                // [2]根据 LUA 脚本字符串创建对象
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script);

                // [3]设定 LUA 脚本返回值类型
                redisScript.setResultType(Long.class);

                // [4]让 Redis 执行 LUA 脚本
                // 参数 1：LUA 脚本对象
                // 参数 2：当前要操作的 key 组成的 List 集合
                // 参数 3：我们要传给 LUA 脚本的数据
                redisTemplate.execute(redisScript, Arrays.asList(lockKey), value);
            }

        } else {

            // 5、如果没有获取到锁，则等待 1 秒后重试
            TimeUnit.SECONDS.sleep(1);

            dataWriteWithLock();

        }

        return "OK";
    }

    @GetMapping("/data/write/redisson")
    public String doDataWriteByRedisson() {

        // 1、设定后面操作需要使用的 key
        // [1]设定业务数据使用的 key
        String dataKey = "lock:test:data:write";

        // [2]设定锁数据使用的 key
        String lockKey = "lock:test:lock:number";

        // 获取锁对象
        // ※RLock 的 R 是 red 意思
        RLock lock = redissonClient.getLock(lockKey);

        boolean lockResult = false;

        try {

            // 2、加锁
            lockResult = lock.tryLock();

            TimeUnit.MINUTES.sleep(5);

            if (lockResult) {
                // 3、执行业务操作
                String number = redisTemplate.opsForValue().get(dataKey);

                if (number == null) {
                    number = "0";
                }

                Integer intNumber = Integer.parseInt(number) + 1;

                redisTemplate.opsForValue().set(dataKey, intNumber.toString());
            } else {
                TimeUnit.SECONDS.sleep(1);
                doDataWriteByRedisson();
            }


        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {

            // 4、解锁
            if (lockResult) {
                lock.unlock();
            }

        }

        return "OK";
    }

}
